﻿using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using System;

namespace xEngine.Rendering.Effects
{
    class PPGaussianBlur
    {
        #region Member Variables

        private SpriteBatch _spriteBatch;
        private RenderTarget2D _tempRT;
        private Effect _shader;

        private Vector2[] _defSampleOffsetsH = new Vector2[15],
                          _defSampleOffsetsV = new Vector2[15],
                          _sampleOffsetsH = new Vector2[15],
                          _sampleOffsetsV = new Vector2[15];

        private float[] _defSampleWeightsH = new float[15],
                        _defSampleWeightsV = new float[15],
                        _sampleWeightsH = new float[15],
                        _sampleWeightsV = new float[15];

        #endregion

        #region Constructors

        public PPGaussianBlur(SpriteBatch spriteBatch, RenderTarget2D tempRT)
        {
            _shader = RenderManager.EmbeddedContent.Load<Effect>("sh_gaussian_blur");
            _spriteBatch = spriteBatch;
            _tempRT = tempRT;

            Vector2 texelSize = new Vector2(1f / RenderManager.Device.PresentationParameters.BackBufferWidth, 1f / RenderManager.Device.PresentationParameters.BackBufferHeight);
            SetGaussianBlurParams(texelSize.X, 0, ref _defSampleOffsetsH, ref _defSampleWeightsH);
            SetGaussianBlurParams(0, texelSize.Y, ref _defSampleOffsetsV, ref _defSampleWeightsV);
        }

        #endregion

        #region Functions

        // Borrowed from http://www.innovativegames.net/blog/blog/2008/12/30/engine-tutorial-10/
        private float ComputeGaussian(float n)
        {
            float theta = 2.0f + float.Epsilon;
            return theta = (float)((1.0 / Math.Sqrt(2 * Math.PI * theta)) * Math.Exp(-(n * n) / (2 * theta * theta)));
        }
        // Borrowed from http://www.innovativegames.net/blog/blog/2008/12/30/engine-tutorial-10/
        private void SetGaussianBlurParams(float dx, float dy, ref Vector2[] vSampleOffsets, ref float[] fSampleWeights)
        {
            // The first sample always has a zero offset.
            fSampleWeights[0] = ComputeGaussian(0);
            vSampleOffsets[0] = new Vector2(0);

            // Maintain a sum of all the weighting values.
            float totalWeights = fSampleWeights[0];

            // Add pairs of additional sample taps, positioned
            // along a line in both directions from the center.
            for (int i = 0; i < 15 / 2; i++)
            {
                // Store weights for the positive and negative taps.
                float weight = ComputeGaussian(i + 1);

                fSampleWeights[i * 2 + 1] = weight;
                fSampleWeights[i * 2 + 2] = weight;

                totalWeights += weight * 2;

                // The 1.5 offset kicks things off by
                // positioning us nicely in between two texels.
                float sampleOffset = i * 2 + 1.5f;

                Vector2 delta = new Vector2(dx, dy) * sampleOffset;

                // Store texture coordinate offsets for the positive and negative taps.
                vSampleOffsets[i * 2 + 1] = delta;
                vSampleOffsets[i * 2 + 2] = -delta;
            }

            // Protects from division by 0
            if (totalWeights == 0)
                return;

            // Normalize the list of sample weightings, so they will always sum to one.
            for (int i = 0; i < fSampleWeights.Length; i++)
                fSampleWeights[i] /= totalWeights;
        }

        public void GaussianBlur(RenderTarget2D input, RenderTarget2D output, bool useFullScreen)
        {
            // Are we using precalculated values?
            if (!useFullScreen)
            {
                Vector2 texelSize = new Vector2(1f / input.Width, 1f / input.Height);
                SetGaussianBlurParams(texelSize.X, 0, ref _sampleOffsetsH, ref _sampleWeightsH);
                SetGaussianBlurParams(0, texelSize.Y, ref _sampleOffsetsV, ref _sampleWeightsV);
            }

            RasterizerState rs = RenderManager.Device.RasterizerState;

            // Horizontal
            _shader.Parameters["offsets"].SetValue(useFullScreen ? _defSampleOffsetsH : _sampleOffsetsH);
            _shader.Parameters["weights"].SetValue(useFullScreen ? _defSampleWeightsH : _sampleWeightsH);

            RenderManager.PushRenderTarget(_tempRT);

            _spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone, _shader);
            _spriteBatch.Draw(input, new Rectangle(0, 0, input.Width, input.Height), Color.White);
            _spriteBatch.End();

            RenderManager.PopRenderTarget();

            // Vertical
            _shader.Parameters["offsets"].SetValue(useFullScreen ? _defSampleOffsetsV : _sampleOffsetsV);
            _shader.Parameters["weights"].SetValue(useFullScreen ? _defSampleWeightsV : _sampleWeightsV);

            RenderManager.PushRenderTarget(output);

            _spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone, _shader);
            _spriteBatch.Draw(_tempRT, new Rectangle(0, 0, input.Width, input.Height), Color.White);
            _spriteBatch.End();

            RenderManager.PopRenderTarget();

            RenderManager.Device.RasterizerState = rs;
        }

        #endregion
    }
}
